[Previous] [Next]

Standard Usage of Forms

In Chapter 2, I described the many properties, methods, and events of Visual Basic forms. In this chapter, I illustrate how forms fit into the object-oriented programming paradigm and how you can exploit them to build effective and bug-free applications.

Forms as Objects

The first step in getting the most out of Visual Basic forms is recognizing what they really are. In all versions from Visual Basic 4 on, in fact, a form is nothing but a class module plus a designer. As you might recall from Chapter 2, designers are modules integrated into the Visual Basic environment that let programmers visually design characteristics of objects to be instantiated at run time.

The form designer lets you define the aspect of your form at design time by placing child controls on its surface and setting their properties. When you launch the application, the Visual Basic runtime translates these pieces of information into a series of calls to Windows API functions that create the main window and then all its child controls. Translated into C or C++ code, a typical Visual Basic form with some controls on it would require several hundred lines of code, which gives you an idea why Visual Basic has quickly become the most popular language for building Windows software.

It's easy to prove that a form is nothing but an object with a user interface. Say that you have a form in your application named frmDetails. You can instantiate it as if it were a regular object using a standard New operator:

Dim frm As frmDetails
Set frm = New frmDetails
frm.Show

A consequence of forms being objects is that they can expose properties, methods, and events exactly as regular objects do. For instance, you can add to a form module one or more Public Property procedures that encapsulate values contained in the form's child controls, as in the following code:

' Inside the frmDetails form 
Public Property Get Total() As Currency
    Total = CCur(txtTotal.Text)
End Property

Similarly, you can define Public methods that let the main application ask the form object to perform an action, for example, to print its contents on the printer:

' Note that Sub, Function, and Property procs are Public by default.
Sub PrintOrders()
    ' Here you place the code that prints the form's contents.
End Sub

From outside the form module, you access the form's properties and methods as you would do with any object:

Dim Total As Currency
Total = frm.Total()
frm.PrintOrders

The most important difference between form modules and regular class modules is that the former can't be made Public and accessed from another application through COM.

Hidden global form variables

The notion that a form is a special type of object leads to an apparent paradox. As you know, to use an object you must first initialize it. But you can (and usually do) reference a form directly without any prior explicit initialization. For example, in the following code snippet you don't need to explicitly create the frmDetails form:

Private Sub cmdDetails_Click()
    frmDetails.Show
End Sub

Since forms are objects, why doesn't this statement raise error 91: "Object variable or With block variable not set"? The reason is mostly historical. When Visual Basic 4 was released, Microsoft engineers were faced with the issue of backward compatibility with Visual Basic 3 and previous versions for which the preceding code was OK. Clearly, if Visual Basic 4 couldn't have imported existing Visual Basic 3 projects, it would have been a flop. The solution the engineers came up with is both simple and elegant. For each form in the current application, the compiler defines a hidden global variable, whose name coincides with that of the form class:

' (Note: you will never actually see these declarations.)
Public frmDetails As New frmDetails
Public frmOrders As New frmOrders
' (Same for every other form in the application)

When your code references the frmDetails entity, you aren't referring to the frmDetails form class, you're referring to a variable whose name happens to be the same as its class. Because this variable is declared to be auto-instancing, Visual Basic creates a new instance of that particular form class as soon as your code references the variable.

This ingenious trick, based on a hidden global form variable, has permitted developers to painlessly port their existing Visual Basic 3 applications to Visual Basic 4 and later versions. At the same time, as you'll see in a moment, these hidden variables introduce a few potential problems that you need to be aware of.

The "clean form instance" problem

To illustrate a problem that often manifests itself when working with forms, I'll create a simple frmLogin form, which asks the end user for his or her name and password and refuses to unload if the password isn't the correct one. This simple form has only two public properties, UserName and Password, which are set to the contents of the txtUserName and txtPassword controls, respectively, in the Unload event procedure. This is the complete source code of the frmLogin form module:

Public UserName As String
Public Password As String

Private Sub cmdOK_Click()
    ' Unload this form only if password is OK.
    If LCase$(txtPassword) = "balena" Then Unload Me
End Sub

Private Sub Form_Load()
    ' Move property values into form fields.
    txtUserName = UserName
    txtPassword = Password
End Sub

Private Sub Form_Unload(Cancel As Integer)
    ' Move field contents back into public properties.
    UserName = txtUserName
    Password = txtPassword
End Sub

You can display the frmLogin form and read its properties to retrieve values entered by the end user:

' Code in frmMain form
Private Sub cmdShowLogin_Click()
frmLogin.Show vbModal
' Execution gets here only if password is OK.
MsgBox frmLogin.UserName & " logged in."
End Sub

To test that this form works correctly, run the main form, click on its cmdShowLogin button, and then enter the correct user name and password. (Mine is shown in Figure 9-1). When the frmMain form regains control, it greets you with a message box. Apparently, everything works as it should. But if you click again on the Login button, the frmLogin form appears again; however, this time the user name and password fields are already filled with the values from the previous call. This isn't what I'd call a secure way to manage passwords!

Click to view at full size.

Figure 9-1. The Login demo application.

To understand what has happened, add the following statements to the frmLogin form module:

Private Sub Form_Initialize()
    Debug.Print "Initialize event"
End Sub
Private Sub Form_Terminate()
    Debug.Print "Terminate event"
End Sub

If you now run the program and repeat the same sequence of actions that I just described, you'll see that the Initialize event is called as soon as you reference the frmLogin variable in code, whereas the Terminate event is never invoked. In other words, the second time you show the frmLogin form, you're actually using the same instance created the first time. The form has been unloaded normally, but Visual Basic hasn't released the instance data area associated with the form instance—that is, the area where Private and Public variables are stored. For this reason, the value of UserName and Password properties persist from the first call, and you'll find them in the two TextBox controls. In a real application, this behavior can lead to bugs that are very difficult to discover because they aren't immediately visible in the user interface.

You can work around this issue by forcing Visual Basic into releasing the form instance so that the next time you reference the form a new instance will be created. You can choose from two methods to achieve this. The most obvious one is to set the form variable to Nothing after returning from the Show method:

Private Sub cmdShowLogin_Click()
    frmLogin.Show vbModal
    MsgBox frmLogin.UserName & " logged in."
    ' Set the hidden global form variable to Nothing.
    Set frmLogin = Nothing
End Sub

The other method is a more object-oriented way to achieve the same result. You simply need to explicitly create a local form variable with the same name as the hidden global variable so that the local variable takes precedence over the global variable:

Private Sub cmdShowLogin_Click()
    Dim frmLogin As New frmLogin
    frmLogin.Show vbModal
    MsgBox frmLogin.UserName & " logged in."
End Sub

If you now run the program, you'll see that when the form variable goes out of scope Visual Basic correctly invokes the form's Form_Terminate event, which is a confirmation that the instance is correctly destroyed: An interesting benefit of this technique is that you can create multiple instances of any nonmodal form, as you can see in Figure 9-2.

Private Sub cmdShowDocument_Click()
    Dim TextBrowser As New TextBrowser
    TextBrowser.Filename = txtFilename.Text
    ' Show the form, making it a child form of this one. 
    TextBrowser.Show, Me
End Sub

Click to view at full size.

Figure 9-2. Using explicit form variables, you can create and display multiple instances of the same form. All child forms are shown in front of their parent form, even if the parent form has the focus.

TIP
The Show method supports a second, optional argument that permits you to specify the parent form of the form being shown. When you pass a value to this argument, you achieve two interesting effects: the child form is always shown in front of its parent, even if the parent has the focus, and when the parent form is closed or minimized, all its child forms are also automatically closed or minimized. You can take advantage of this feature to create floating forms that host a toolbar, a palette of tools, a group of icons, and so on. This technique is most effective if you set the BorderStyle property of the child form to 4-Fixed ToolWindow or 5-Sizable ToolWindow.

You can find the complete source code of the frmTextBrowser form module on the companion CD. Note that in this case you're working with modeless forms. Consequently, when the form variable goes out of scope the form is still visible, which prevents Visual Basic from releasing the instance data area. When eventually the end user unloads the form, the Visual Basic runtime fires the Form_Terminate event immediately after the Form_Unload event. This seems to break the rule that any object is released as soon as the program destroys the last reference to it, but we haven't really destroyed the last reference, as I'll explain next.

The Forms collection

The Forms collection is a global collection that contains all the currently loaded forms. This means that all loaded forms are referenced by this collection, and this additional reference keeps the form alive even if the main application has released all the references to a form. You can exploit the Forms collection to retrieve a reference to any form, even though the application has set to Nothing all the other references to it. All you need is the following function:

Function GetForm(formName As String) As Form
    Dim frm As Form
    For Each frm In Forms
        If StrComp(frm.Name, formName, vbTextCompare) = 0 Then
            Set GetForm = frm
            Exit Function
        End If
    Next
End Function

If there are multiple occurrences of the same form, the preceding function returns the first reference to it in the Forms collection. You can use this function to reference a form by its name, as in this code:

GetForm("frmLogin").Caption = "Login Form"

You should be aware that the GetForm function returns a reference to a generic Form object. It therefore exposes the interface common to all forms, which includes properties such as Caption and ForeColor and methods such as Move and Show. You can't use this interface to access any custom method or property you have defined for a particular form class. Instead, you must cast the generic Form reference to a specific variable:

Dim frm As frmLogin
Set frm = GetForm("frmLogin") = "Login Form"
username = frm.UserName

Reusable Forms

The notion that forms are objects suggests that you can reuse them exactly as you reuse class modules. You can reuse forms many ways, the simplest one being to store them as templates, as I suggested in Chapter 2. But you can take advantage of more advanced and flexible techniques for form code reuse. I'll describe these techniques in the following sections.

Using custom properties and methods

Many business applications show a calendar for the user to select one or more dates. Visual Basic comes with a MonthView Microsoft ActiveX control (see Chapter 11), but a custom form has its advantages: Just to name a few, you can customize its size, the language used for months and the names of days of the week, and colors used for holidays. In general, a custom form gives you the greatest flexibility and control over the user interface. On the companion CD, you'll find the complete source code for the frmCalendar form module shown in Figure 9-3. The buttons with day numbers are arranged in an array of OptionButton controls whose Style property is set to 1-Graphical.

Click to view at full size.

Figure 9-3. A custom Calendar form that communicates with the main application through custom properties, methods, and events.

The frmCalendar form exposes several properties that let you customize its interface, such as DialogTitle (the caption of the dialog box), FirstDayOfWeek, and SaturdayIsHoliday (useful for customizing the appearance of the calendar). There are also properties for retrieving the date selected by the user: CancelPressed (True if the end user hasn't selected any date), SelectedDate (a read/write Date value), Day, Month, and Year (read-only properties that return a given component of SelectedDate). The module exposes a single method, ShowMonth, that displays a given month in the dialog box, and optionally highlights a particular day:

Private Sub cmdCalendar_Click()
    Dim Calendar As New frmCalendar
    Calendar.DialogTitle = "Select a new date for the appointment"
    ' Highlight the current day/month/year.
    Calendar.ShowMonth Year(Now), Month(Now), Day(Now)
    ' Show the calendar as a modal dialog.
    Calendar.Show vbModal
    ' Get the result if the user didn't press Cancel.
    If Not Calendar.CancelPressed Then
        AppointmentDate = Calendar.SelectedDate
    End If
End Sub

In general, when working with a form as an object, you should provide the form with an interface that lets you avoid accessing the form's native properties. For example, the frmCalendar form module exposes the DialogTitle property, and client code should use it instead of the standard Caption property. This way, the client doesn't break the form object encapsulation, which in turn makes it possible for you to have control of what happens inside the form module. Alas, while you can build really robust class modules, you have no way to prevent the application from directly accessing the form's native properties or the controls on the form's surface. Nevertheless, you should exercise this discipline if you want to enjoy all the benefits of using forms as objects.

Adding custom events

In the previous code example, the frmCalendar form is displayed as a modal dialog box, which pauses the execution of the program until the dialog box is closed. When the dialog box closes, you can query the form's properties to retrieve the end user's choices. In many circumstances, however, you might want to display a form as a modeless dialog box. In this case, you need a way to learn when the user closes the form so that you can query its SelectedDate property. You can accomplish this by adding a couple of custom events to the form module:

Event DateChanged(newDate As Date) 
Event Unload(CancelPressed As Boolean)

These custom events increase the usability of the frmCalendar module. To trap these custom events, you need a module-level WithEvents variable in the form that's showing the calendar dialog box, as you would for a regular object:

' In the frmMain form
Dim WithEvents Calendar As New frmCalendar

Private Sub cmdCalendar_Click()
    Set Calendar = New frmCalendar
    Calendar.DialogTitle = "Select a new date for the appointment"
    Calendar.ShowMonth Year(Now), Month(Now), Day(Now)
    Calendar.Show           ' Show as a modeless dialog box.
End Sub

Private Sub Calendar_DateChanged(newDate As Date)
    ' Show the date currently selected on a Label control.
    lblStatus.Caption = Format(newDate, "Long Date")
End Sub

Private Sub Calendar_Unload(CancelPressed As Boolean)
    If CancelPressed Then
        MsgBox "Command canceled", vbInformation
    Else
        MsgBox "Selected date: " & Format$(Calendar.SelectedDate, _
            "Long Date"), vbInformation
    End If
    ' We don't need this variable any longer.
    Set Calendar = Nothing
End Sub

NOTE
You might wonder why you need a custom Unload event: since the Calendar variable is referencing the frmCalendar form, you might think it capable of trapping its Unload event. This assumption isn't correct because, in fact, the Calendar variable is pointing to the frmCalendar interface, whereas the Unload event as well as other form events such as Resize, Load, Paint, and so on are exposed by the Form interface and can't be trapped by the frmCalendar variable. If you want to trap standard form events, you should assign the form reference to a generic Form variable. In this particular case, adding a custom Unload event simplifies the structure of the client code.

As they do for regular class modules, custom events add a lot of flexibility to form modules. Adding a Change-like event—such as the DateChanged event in the frmCalendar module—lets you keep the application in sync with data entered by the end user in the form. You can add many other types of events, for instance a Progress event, for when the form module performs lengthy operations. For more information about events, see Chapter 7.

Parameterized forms

You can push the usability of form modules even further with the concept of parameterized forms, a name that I use for forms whose appearance heavily depends on how the main application sets the properties or invokes the methods of the form before actually showing it. To see what I mean, have a look at Figure 9-4: the two Options forms are actually the same form module, which adjusts itself according to what the main application has requested.

Parameterized forms are difficult to build, for two main reasons. First, you need to provide a reasonable set of properties and methods that let the client code customize the form appearance and contents and eventually retrieve all the values entered by the user. Second, you have to write a lot of code within the form to create controls on the fly and automatically place them in their appropriate positions on the form.

Click to view at full size.

Figure 9-4. Two distinct instances of a parameterized Options form.

The frmOptions form exposes three key methods, which let you add a Frame control, a CheckBox control, and an OptionButton control:

Private Sub cdmOptionsOne_Click()
    Dim frm As New frmOptions

    ' Add a Frame control _ the first argument to this and following 
    ' methods is a unique ID code for the control being created.
    frm.AddFrame "F1", "First Group"
    ' Each subsequent AddOption and AddCheck method adds 
    ' a control inside the current frame, until another AddFrame
    ' method is issued.
    frm.AddOption "O1", "&1. First", True   ' Set the value to True.
    frm.AddOption "O2", "&2. Second"
    frm.AddOption "O3", "&3. Third"

    ' Add a second frame, with three radio buttons and two check boxes.
    frm.AddFrame "F2", "Second Group"
    frm.AddOption "O4", "&4. Fourth", True  ' Set the value to True.
    frm.AddOption "O5", "&5. Fifth"
    frm.AddOption "O6", "&6. Sixth"
    ' Tick this check box control.
    frm.AddCheck "C1", "&7. Check one", True  
    frm.AddCheck "C2", "&8. Check two"
    ' Show the form as a modal dialog.
    frm.Show vbModal

The form module exposes the Value method, which returns the value of a control given its ID code. You can use it as you would use the Value property for CheckBox and OptionButtons controls, or you can pass it the ID code of a Frame control to learn which OptionButton control is selected inside the frame itself:

    ' Continuing the cmdOptionsOne_Click procedure...
    If frm.CancelPressed Then
        MsgBox "Command canceled", vbInformation
    Else
        MsgBox "Option button in first frame: " & frm.Value("F1") _
            & vbCr &  "Option button in second frame: " _
            & frm.Value("F2") & vbCr _
            & "First checkbox : " & frm.Value("C1") & vbCr _
            & "Second checkbox: " & frm.Value("C2") & vbCr, _
            vbInformation, "Result of Options form"
    End If
End Sub

Have a look at the source code of the frmOptions form module to see how it resizes each Frame control to account for all its contained controls. You can also see how the form itself is resized to account for all the Frame controls on it.

You can build a huge number of parameterized forms like the frmOptions module. For example, you can use forms for showing custom message boxes with any number of buttons, any icon, any font for the main message text, and so on. The greatest advantage of parameterized forms is that you build them once and reuse them for forms and dialog boxes that behave in the same or a similar way, even if their appearance is different. This has a beneficial impact on the size of the EXE file and on the memory needed at run time.

Forms as Object Viewers

You can look at forms in yet another way. If your application extensively uses class modules for storing and processing data, you might build forms that work as specialized object viewers. For example, if you have a CPerson class module that holds personal data, you might build a frmPerson form module that exposes one custom object property—Person, of type CPerson. This approach greatly simplifies the structure of the client code because it just needs to assign one single property instead of many distinct, simpler properties (in this case, Name, Address, City, and Married):

' The client code that uses the frmPerson form
Dim Person1 As New CPerson
' Initialize properties for this instance.
Person1.Name = "John Smith"
Person1.Address = "12345 West Road"
...
' Display it on screen.
Dim frm As New frmPerson
Set frm.Person = Person1
frm.Show

The frmPerson form module has to correctly assign values to its fields when the Person property is set, as you can see in Figure 9-5 and in the code that follows it.

Click to view at full size.

Figure 9-5. Using forms as object viewers. The two form instances that display the same CPerson object are automatically synchronized.

' Inside the frmPerson form module
Private WithEvents ThisPerson As CPerson

Property Get Person() As CPerson
    Set Person = ThisPerson
End Property
Property Set Person(newValue As CPerson)
    ' Initialize the private object and form fields.
    Set ThisPerson = newValue
    With ThisPerson
        txtName.Text = .Name
        txtAddress.Text = .Address
        txtCity.Text = .City
        chkMarried.Value = Abs(.Married)    ' Assign zero or one.
    End With
End Property

Another advantage of this technique is that the client code doesn't directly address the properties of the CPerson object. Thanks to this detail, you must add or remove statements in the frmPerson module if the interface exposed by this class changes, but you don't have to modify the code in the client application that instantiates the frmPerson module showing the object.

A third, more interesting advantage of this approach is this: Because the form has a direct link with the class that holds the data, the form can delegate all the data validation chores to the class itself, which is the right thing to do in an object- oriented application. The validation process usually occurs when the user clicks on the OK button:

' In the frmPerson form module...
Private Sub cmdOK_Click()
    On Error Resume Next
    ' Assign (and implicitly validate) the Name property.
    ThisPerson.Name = txtName.Text
    If Err Then
        ' If the class raised an error
        MsgBox Err.Description
        txtName.SetFocus
        Exit Sub
    End If

    ' Similar code for the Address, City, and Married properties.
    ...
End Sub

Forms used as object viewers have a fourth advantage, which is, in my opinion, also the most important and intriguing one. Since each form holds a reference to the actual object, you can have multiple forms pointing to the same object. This ensures that all form instances access the same data and that they don't display inconsistent values. To see what I mean, run the ObjView.Vbp sample application, click two or more times on the John Smith button, modify data in a form, and then click on OK to see the new value automatically propagated to all the other form instances. By selecting a different option in the Notification frame on the main form, you can also have new values propagated to other forms whenever the user exits each field (field-level notification) or even when the user presses a key (key-level notification). I added this capability to the demonstration program just to show you that it's possible to do it. But in most real-world applications, record-level notification is the most appropriate choice.

To implement this fourth feature, the CPerson class raises an event whenever one of its properties changes, as shown here:

' In the CPerson class module...
Event Change(PropertyName As String)
' A private variable that holds the value of the Name property
Private m_Name As String

Property Let Name(newValue As String)
    ' It's very important that the new value always be checked.
    If newValue = "" Then Err.Raise 5, , "Invalid Value for Name property"
    If m_Name <> newValue Then
        m_Name = newValue
        PropertyChanged "Name"
    End If
End Property

' Similar code for Property Let Address/City/Married
... 

' This private method simply raises a Change event in client code.
Private Sub PropertyChanged(PropertyName As String)
    RaiseEvent Change(PropertyName)
End Sub

The frmPerson form module can trap the Change event because its ThisPerson Private instance pointing to the CPerson object is declared using the WithEvents keyword:

Private Sub ThisPerson_Change(PropertyName As String)
    Select Case PropertyName
        Case "Name"
            txtName.Text = ThisPerson.Name
        Case "Address"
            txtAddress.Text = ThisPerson.Address
        Case "City"
            txtCity.Text = ThisPerson.City
        Case "Married"
            chkMarried.Value = Abs(ThisPerson.Married)
    End Select
End Sub

Here are a couple of final notes about using forms as object viewers:

Dynamic Control Creation

Dynamic control creation is one of the most exciting, new features of Visual Basic 6 and overcomes a serious limitation of the previous versions of the language. Using this new capability, you can create new controls on a form at run time by specify-ing their class name. This mechanism is much more flexible than the one based on control arrays (described in Chapter 3). In fact, creating a control at run time using control arrays forces you to place an instance of each type of control on the form at design time. This isn't necessary using Visual Basic 6's dynamic control creation features.

The Add method of the Controls collection

Under Visual Basic 6, the Controls collection has been enhanced to support the Add method, which lets you dynamically create controls at run time. This method uses the following syntax:

Set controlRef = Controls.Add(ProgID, Name [,Container])

where ProgID is the class name of the control in the format libraryname.controlname, and Name is the name you want to assign to the control (and which will be returned by its Name property). This name must be unique: If another control in the collection has the same name, Visual Basic raises an error 727—"There is already a control with the name 'ctrlname'". Container is an optional reference to a container control (for example, a PictureBox or a Frame control) inside which you want to place the control being created. If you omit this argument, the control is placed on the form's surface. ControlRef is an object variable that you use to reference the control's properties, invoke its methods, and trap its events. You can see from the following code how easy it is to create a CommandButton control and place it near the lower right corner of the form:

Dim WithEvents cmdCalendar As CommandButton

Private Sub Form_Load()
    Set cmdCalendar = Controls.Add("VB.CommandButton", "cmdButton")
    ' Assumes that form's ScaleMode is twips.
    cmdCalendar.Move ScaleWidth - 1400, ScaleHeight - 800, 1000, 600
    cmdCalendar.Caption = "&Calendar"
    ' All controls are created invisible.
    cmdCalendar.Visible = True
End Sub

Because you have declared cmdCalendar using the WithEvents clause, you can react to its events. For example, you can display a custom calendar when the user clicks on the button you just created:

Private Sub cmdCalendar_Click()
    Dim frm As New frmCalendar
    frm.ShowMonth Year(Now), Month(Now)
    frm.Show vbModal
End Sub

You can remove any control added dynamically using the Controls collection's Remove method, whose only argument is the name of the control (that is, the string passed as a second argument to the Add method):

Controls.Remove "cmdButton"

You get an error if the control specified doesn't exist on the form or if it wasn't added dynamically at run time.

Adding an external ActiveX control

Adding an external ActiveX control is similar to adding an intrinsic Visual Basic control. But you must pay particular attention to two important details.

NOTE
You can dynamically add any type of intrinsic Visual Basic control, except menu items. Unfortunately, this limitation prevents developers from devising customizable menu structures with top-level menus and submenus built on the fly.

The Windowless Controls Library

Visual Basic 6 comes with a new library of windowless controls that exactly duplicate the appearance and the features of most Visual Basic intrinsic controls. This library isn't mentioned in the main language documentation, and it must be installed manually from the Common\Tools\VB\Winless directory. This folder contains the Mswless.ocx ActiveX control and the Ltwtct98.chm file with its documentation. To install the library, you first copy this directory on your hard disk. Before you can use the control, you must register it using the Regsvr32.exe utility or from within Visual Basic, and then double-click on the Mswless.reg file, which creates the Registry keys that make the ActiveX control available to the Visual Basic environment.

Once you have completed the registration step, you can load the library into the IDE by pressing the Ctrl+T key and selecting the Microsoft Windowless Controls 6 item from the list of available ActiveX controls. After you do this, you'll find that a number of new controls have been added to the Toolbox. The library contains a replacement for the TextBox, Frame, CommandButton, CheckBox, OptionButton, ComboBox, ListBox, and the two ScrollBar controls. It doesn't include Label, Timer, or Image controls because the Visual Basic versions are already windowless. Nor does it contain PictureBox and OLE controls, which are containers and can't therefore be rendered as windowless controls.

The controls in the Windowless Controls Library don't support the hWnd property. As you might remember from Chapter 2, this property is the handle of the window on which the control is based. Since these controls are windowless, there's no such window and therefore the hWnd property doesn't make any sense. Other properties are missing, namely those that have to do with DDE communications. (DDE is, however, an outdated technology and isn't covered in this book.) Another difference is that the WLOption control (the windowless counterpart of the OptionButton intrinsic control) supports the new Group property, which serves to create groups of mutually exclusive radio buttons. (You can't create a group of radio buttons by placing them in a WLFrame control because this control doesn't work as a container.)

Apart from the hWnd property and the Group property, the controls in the library are perfectly compatible with Visual Basic's intrinsic controls in the sense that they expose the same properties, methods, and events as their Visual Basic counterparts. Interestingly, the library's controls offer a number of property pages that let the programmer set the properties in a logical manner, as you can see in Figure 9-8.

Click to view at full size.

Figure 9-8. You can set the properties of controls in the Windowless library using handy property pages. Notice how the new controls appear in the Toolbox.

The real advantage of using the controls in the Windowless library is that at run time they aren't subject to many of the limitations that the intrinsic controls are. In fact, all their properties can be modified during execution, including the MultiLine and ScrollBars properties of the WLText control, the Sorted and Style properties of the WLList and WLCombo controls, and the Alignment property of the WLCheck and WLOption controls.

The ability to modify any property at run time makes the Windowless library a precious tool when you're dynamically creating new controls at run time using the Controls.Add method. When you add a control, it's created with all properties set to their default values. This situation means that you can't use the Controls.Add method to create multiline intrinsic TextBox controls or sorted ListBox or ComboBox controls. The only solution is to use the Windowless Controls Library:

Dim WithEvents TxtEditor As MSWLess.WLText

Private Sub Form_Load()
    Set TxtEditor = Controls.Add("MSWLess.WLText", "txtEditor")
    TxtEditor.MultiLine = True
    TxtEditor.ScrollBars = vbBoth
    TxtEditor.Move 0, 0, ScaleWidth, ScaleHeight
    TxtEditor.Visible = True
End Sub

Unreferenced controls

So far, I've described what you have to do to add controls that are referenced at design time in the Toolbox. But you can do more with the dynamic control creation feature than I've shown you so far; its greater power lies in letting you create ActiveX controls that aren't referenced in the Toolbox. You can provide support for versions of ActiveX controls that don't exist yet at compile time, for example by storing the control's name in an INI file that you edit when delivering a new version of the control. This adds tremendous flexibility to your applications and lets you transform your forms into generic ActiveX control containers.

The first issue you must resolve when working with controls not referenced in the Toolbox is design-time licensing. Even if you're not actually using the control at design time, to dynamically load it at run time you must prove that you're legally allowed to do so. If there weren't any restrictions to dynamically creating ActiveX controls at run time, any programmer could "borrow" ActiveX controls from other commercial software and use them in his or her applications without actually purchasing the license for the controls. This is an issue only for ActiveX controls that aren't referenced in the Toolbox at design time; if you can load a control in the Toolbox, you surely own a design-time license for the control.

To dynamically create an ActiveX control not referenced in the Toolbox at compile time, you must exhibit your design-time license at run time. In this context, a license is a string of characters or digits that comes with the control and is stored in the system Registry when you install the control on your machine. Visual Basic doesn't force you to search for this string in the Registry because you can find it by means of the Add method of the Licenses collection:

' This statement works only if the MSWLess library is
' *NOT* currently referenced in the Toolbox.
Dim licenseKey As String
licenseKey = Licenses.Add("MSWLess.WLText")

After you have the license string, you must devise a way to make it available to the application at run time. The easier method is storing it in a file:

Open "MSWLess.lic" For Output As #1
Print #1, licenseKey
Close #1

The preceding code must be executed just once during the design process, and after you've generated the LIC file you can throw the code away. The application reads this file back into the Licenses collection, again using the Add method but this time with a different syntax:

Open "MSWLess.lic" For Input As #1
Line Input #1, licenseKey
Close #1
Licenses.Add "MSWLess.WLText", licenseKey

The Licenses collection also supports the Remove method, but you will rarely need to invoke it.

Late-bound properties, methods, and events

Once you resolve the licensing issue, you're ready to face another problem that comes up when you're working with ActiveX controls not referenced in the Toolbox at compile time. As you might imagine, if you don't know what control you'll load at run time, you can't assign the return value of the Controls.Add method to an object variable of a specific type. This means that you have no simple way to access properties, methods, or events of your freshly added control.

The solution offered by Visual Basic 6 is a special type of object variable named VBControlExtender. This represents a generic ActiveX control inside the Visual Basic IDE:

Dim WithEvents TxtEditor As VBControlExtender

Private Sub Form_Load()
    ' Add the license key to the Licenses collection (omitted).
    Set TxtEditor = Controls.Add("MSWLess.WLText", "TxtEditor")
    TxtEditor.Move 0, 0, ScaleWidth, ScaleHeight
    TxtEditor.Visible = True
    TxtEditor.Text = "My Text Editor"
End Sub

Trapping events from an ActiveX control not referenced in the Toolbox is a bit more complex than accessing properties and methods. In fact, the VBControlExtender object can't expose the events of the control it will host at run time. Instead, it supports only a single event, named ObjectEvent, which is invoked for all the events raised by the original ActiveX control. The ObjectEvent event receives one argument, an EventInfo object that in turn contains a collection of EventParameter objects. This collection enables the programmer to learn what arguments were passed to the event.

Click to view at full size.

Inside the ObjectEvent event procedure, you usually test the EventInfo.Name property to discern which event was fired, and then you read, and sometimes modify, the value of each of its parameters:

Private Sub TxtEditor_ObjectEvent(Info As EventInfo)
    Select Case Info.Name
        Case "KeyPress"
            ' The Escape key clears the editor.
            If Info.EventParameters("KeyAscii") = 27 Then
                TxtEditor.Object.Text = ""
            End If
        Case "DblClick"
            ' Just to prove that we can trap any event
            MsgBox "Why have you double-clicked me?"
    End Select
End Sub

Events trapped in this way are called late-bound events. There's a group of extender events that you don't trap inside the ObjectEvent event. These extender events (one of which is shown in the following code snippet) are available as regular events of the VBControlExtender object. This group of events includes GotFocus, LostFocus, Validate, DragDrop, and DragOver. For more information about extender properties, methods, and events, see Chapter 17.

Private Sub TxtEditor_GotFocus()
    ' Highlight textbox's contents on entry.
    TxtEditor.Object.SelStart = 0
    TxtEditor.Object.SelLength = 9999
End Sub

Data-Driven Forms

Visual Basic's new dynamic control creation capabilities enable developers to create true data-driven forms, which are forms whose appearance is completely determined at run time by data read from a file or—if you're building a form that displays data from a database table—by the structure of the database itself. Imagine what degree of flexibility you get if you're able to modify the appearance of a Visual Basic form at run time without having to recompile the application:

To implement data-driven forms, however, you must first solve a problem. When you don't know in advance how many controls you're going to add to the form, how can you trap events from them? This problem arises because the WithEvents keyword is unable to trap events from an array of objects. As you'll see, this issue can be resolved, but the solution probably isn't as simple as you might think it should be. However, the technique that I'll describe is both interesting and flexible and can not only help you build data-driven forms but also more generally trap events from an undetermined number of objects, a problem I left unresolved in Chapter 7.

Trapping events from an array of controls

To trap events coming from an undetermined number of controls dynamically created at run time—or, in general, from an undetermined number of objects—you need to build two support classes, the first one a collection class that contains all the instances of the second class. In the sample program that you'll find on the companion CD, these classes are named ControlItems and ControlItem, respectively. The relationships among these classes and the main form are summarized in Figure 9-9.

Click to view at full size.

Figure 9-9. You need two auxiliary classes and some tricky code to trap events raised by an array of controls dynamically created at run time.

The events can be trapped as follows:

  1. The main application holds a reference to an instance of the ControlItems collection class in a variable named CtrlArray. This variable is declared using the WithEvents clause because it will raise events in the main application.
  2. After the form creates a new control, it passes a reference to that control to the Add method of the ControlItems collection class. This reference can be of a specific class (if you know at design time which type of controls you're creating) or it can be of a VBControlExtender object if you want to exploit late-bound events.
  3. The Add method of the ControlItems collection class creates a new instance of the ControlItem class and passes it a reference to the control just created on the form. It also passes a reference to itself.
  4. The ControlItem class instance stores the reference to the control in a WithEvents Public variable. It also stores a reference to the parent ControlItems collection class in the Private Parent object variable.
  5. When the control eventually raises an event, the ControlItem class traps it and can therefore pass the event to the parent collection classes. This notification is performed by calling a Friend method in the ControlItems collection class. In general, you should provide one such method for each possible event trapped by the dependent class because each event has a different set of arguments.
  6. Inside the notification event, the ControlItems class can finally raise an event in the parent form. The first argument passed to this event is a reference to the control that raised the event or a reference to the ControlItem object that trapped it.

As you can see, it's a long trip just to intercept an event. But now that you know how to do it, you can apply this technique in many interesting ways.

Database-driven data-entry forms

One of the many possible applications of dynamic control creation are forms that automatically map themselves to the structure of a database table or query. This is especially useful when you're writing large business applications with dozens or hundreds of queries, and you don't want to create customized forms for each one. This technique dramatically reduces development time and shrinks the size of the executable file as well as its requirements in terms of memory and resources.

On the companion CD, you'll find a complete Visual Basic application whose main form adapts itself to the structure of a database table of SQL SELECT query, as you can see in Figure 9-10.

Click to view at full size.

Figure 9-10. All the controls on this form are dynamically created at run time, based on the structure of an ADO recordset. The program creates different controls according to the type of the database field and also provides validation for each.

Space constraints prevent me from showing the complete source code in print, so I'll include only the most interesting routines:

' The collection of controls added dynamically (module-level
' variable)
Dim WithEvents ControlItems As ControlItems

' This is the most interesting routine, which actually
' creates the controls and passes them to the ControlItems
' collection class.
Sub LoadControls(rs As ADODB.Recordset)
    Dim index As Long, fieldNum As Integer
    Dim field As ADODB.field
    Dim ctrl As Control, ctrlItem As ControlItem, ctrlType As String
    Dim Properties As Collection, CustomProperties As Collection
    Dim top As Single, propItem As Variant
    Dim items() As String
     
    ' Start with a fresh ControlItems collection.
    Set ControlItems = New ControlItems
    ' Initial value for Top property
    top = 100

    ' Add controls corresponding to fields.
    ' This demo program supports only a few field types.
    For Each field In rs.Fields
        ctrlType = ""
        Set Properties = New Collection
        Set CustomProperties = New Collection
        Select Case field.Type
            Case adBoolean
                ctrlType = "MSWLess.WLCheck"
                Properties.Add "Caption="
            Case adSmallInt   ' As Integer
                ctrlType = "MSWLess.WLText"
            Case adInteger    ' As Long
                ctrlType = "MSWLess.WLText"
                CustomProperties.Add "IsNumeric=-1"
                CustomProperties.Add "IsInteger=-1"
            Case adSingle, adDouble, adCurrency
                ctrlType = "MSWLess.WLText"
                CustomProperties.Add "Numeric=-1"
            Case adChar, adVarChar  ' As String
                ctrlType = "MSWLess.WLText"
                Properties.Add "Width=" & _
                    (field.DefinedSize * TextWidth("W"))
            Case adLongVarChar   ' (Memo field)
                ctrlType = "MSWLess.WLText"
                Properties.Add "Width=99999"  ' Very large width
                Properties.Add "Height=2000"
                Properties.Add "Multiline=-1"
                Properties.Add "ScrollBars=2"  'vbVertical
            Case adDate
                ctrlType = "MSWLess.WLText"
                Properties.Add "Width=1000"
                CustomProperties.Add "IsDate=-1"
            Case Else
                ' Ignore other field data types.
        End Select

        ' Do nothing if this field type is not supported (ctrlType="").
        If ctrlType <> "" Then
            fieldNum = fieldNum + 1
            ' Create the label control with database field name.
            Set ctrl = Controls.Add("VB.Label", "Label" & fieldNum)
            ctrl.Move 50, top, 1800, 315
            ctrl.Caption = field.Name
            ctrl.UseMnemonic = False
            ctrl.BorderStyle = 1
            ctrl.Alignment = vbRightJustify
            ctrl.Visible = True
            ' Create the control, and move it to the correct position.
            Set ctrl = Controls.Add(ctrlType, "Field" & fieldNum)
            ctrl.Move 1900, top, 2000, 315

            ' If the field is not updatable, lock it. 
            If (field.Attributes And adFldUpdatable) = 0 Then
                On Error Resume Next
                ctrl.Locked = True
                ' If the control doesn't support the Locked property,
                ' disable it.
                If Err Then ctrl.Enabled = False
                On Error GoTo 0
            End If

            ' Set other properties of the field.
            For Each propItem In Properties
                ' Split property's name and value.
                items() = Split(propItem, "=")
                CallByName ctrl, items(0), VbLet, items(1)
            Next
            ' Link it to the Data control, and make it visible.
            Set ctrl.DataSource = Adodc1
            ctrl.DataField = field.Name
            ctrl.Visible = True

            ' Add this control to the ControlItems collection.
            Set ctrlItem = ControlItems.Add(ctrl)
            ' Move the actual width into the custom Width property.
            ' This is used in the Form_Resize event.
            ctrlItem.Properties.Add ctrl.Width, "Width"
            ' Set its other custom properties.
            For Each propItem In CustomProperties
                ' Split property name and value.
                items() = Split(propItem, "=")
                ctrlItem.Properties.Add items(1), items(0)
            Next
            ' Increment top.
            top = top + ctrl.Height + 80
        End If
    Next
    ' Force a Form_Resize event to resize longer controls.
    Call Form_Resize
    Adodc1.Refresh
End Sub

' A control added dynamically is asking for validation.
' Item.Control is a reference to the control.
' Item.GetProperty(propname) returns a custom property.
Private Sub ControlItems_Validate(Item As ControlItem, _
    Cancel As Boolean)
    If Item.GetProperty("IsNumeric") Then
        If Not IsNumeric(Item.Control.Text) Then
            MsgBox "Please enter a valid number"
            Cancel = True: Exit Sub
        End If
    End If
    If Item.GetProperty("IsInteger") Then
        If CDbl(Item.Control.Text) <> Int(CDbl(Item.Control.Text)) Then
            MsgBox "Please enter a valid Integer number"
            Cancel = True: Exit Sub
        End If
    End If
    If Item.GetProperty("IsDate") Then
        If Not IsDate(Item.Control.Text) Then
            MsgBox "Please enter a valid date"
            Cancel = True: Exit Sub
        End If
    End If
End Sub

Many points in the LoadControls routine are worth a closer look. First, it uses the Windowless Controls Library because it needs to modify properties such as TextBox control's Multiline (for example, for memo fields). Second, to streamline the structure of the code and make it easily extendable, each Case clause in the main Select block simply adds property names and values to a Properties collection: after the control is actually created, it uses the CallByName command to assign all the properties in a For Each loop. Third, it creates the CustomProperties collection, where it stores information that can't be directly assigned to the control's properties. This includes the "IsNumeric", "IsInteger", and "IsDate" custom attributes, which are later used when the code in the main form validates the value in the field.

Please refer to the complete project on the companion CD for the complete source code of the main form and the ControlItems and ControlItem class modules.